package com.gitee.wsl.jvm.io.resources

import timber.log.Timber
import java.io.IOException
import java.lang.AutoCloseable
import java.net.URI
import java.net.URISyntaxException
import java.net.URL
import java.nio.file.ClosedFileSystemException
import java.nio.file.Files
import java.nio.file.Path
import java.util.Collections
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Function
import java.util.stream.Stream


/**
 * Class to be only used on startup to load all resources (SQL migrations and noSQL migrations) from the classpath.
 *
 * As Jar files need to be mounted as FileSystems which are static, this class uses explicit locking to ensure that only one
 * consumer can access the resources at a time. It must thus always be used in a try-with-resources block.
 */
class ClassPathResourceProvider : AutoCloseable {
    init {
        try {
            lock.tryLock(5, TimeUnit.SECONDS)
        } catch (e: InterruptedException) {
            throw IllegalStateException(
                "Unable to open lock. Make sure the ClassPathResourceProvider is used inside a try-with-resources block?",
                e
            )
        }
    }


    fun listAllChildrenOnClasspath(clazz: Class<*>, vararg subFolder: String): Stream<Path?> {
        try {
            return toPathsOnClasspath(clazz, *subFolder)
                .flatMap<Path?>(Function<Path, Stream<out Path?>> { rootPath: Path? ->
                    this.listAllChildrenOnClasspath(
                        rootPath
                    )
                })
        } catch (e: Exception) {
            throw shouldNotHappenException(e)
        }
    }

    private fun toPathsOnClasspath(clazz: Class<*>, vararg subFolders: String): Stream<Path> {
        return toPathsOnClasspath(clazz.getPackage(), *subFolders)
    }

    private fun toPathsOnClasspath(pkg: Package, vararg subFolders: String): Stream<Path> {
        val joinedSubfolders = java.lang.String.join("/", *subFolders)
        if (joinedSubfolders.startsWith("/")) {
            return toUrls(joinedSubfolders.substring(1))
                .map { url: URL -> this.toPath(url) }
        }
        return toUrls(pkg.name.replace(".", "/") + "/" + joinedSubfolders)
            .map { url: URL -> this.toPath(url) }
    }

    private fun toUrls(folder: String): Stream<URL> {
        try {
            val classLoader = Thread.currentThread().contextClassLoader
            val resources = classLoader.getResources(folder)

            return Collections.list(resources).stream()
        } catch (e: IOException) {
            throw shouldNotHappenException(e)
        }
    }

    private fun toPath(url: URL): Path {
        try {
            var uri = url.toURI()
            if ("wsjar" == uri.scheme) { // support for openliberty
                uri = URI(uri.toString().replace("wsjar", "jar"))
            }

            val fileSystemProvider = getFileSystemProvider(uri)
            return fileSystemProvider.toPath(uri)
        } catch (e: IOException) {
            throw shouldNotHappenException(e)
        } catch (e: URISyntaxException) {
            throw shouldNotHappenException(e)
        }
    }

    private fun shouldNotHappenException(e: Exception): Throwable {
        return e
    }

    private fun listAllChildrenOnClasspath(rootPath: Path?): Stream<Path?> {
        try {
            if (rootPath == null) return Stream.empty()
            if (Files.notExists(rootPath)) return Stream.empty()

            return Files.list(rootPath)
        } catch (e: IOException) {
            throw shouldNotHappenException(e)
        }
    }

    private fun getFileSystemProvider(uri: URI): FileSystemProvider {
        return fileSystemProviders.computeIfAbsent(uri.scheme) { scheme: String ->
            this.getFileSystemProviderByScheme(
                scheme
            )
        }
    }

    @Throws(IllegalStateException::class)
    override fun close() {
        try {
            for (fileSystemProvider in fileSystemProviders.values) {
                closeFileSystemProvider(fileSystemProvider)
            }
        } catch (e: Exception) {
            Timber.e("Could not close FileSystemProvider", e)
            throw IllegalStateException("Could not close FileSystemProvider", e)
        } finally {
            fileSystemProviders.clear()
            lock.unlock()
        }
    }

    private fun getFileSystemProviderByScheme(scheme: String): FileSystemProvider {
        return when (scheme) {
            "jar" -> JarFileSystemProvider()
            "resource" -> ResourcesFileSystemProvider()
            "file" -> PathFileSystemProvider()
            else -> throw IllegalArgumentException("Unknown FileSystem required $scheme")
        }
    }

    @Throws(IOException::class)
    private fun closeFileSystemProvider(fileSystemProvider: FileSystemProvider) {
        try {
            fileSystemProvider.close()
        } catch (e: ClosedFileSystemException) {
            // ignore
        }
    }

    companion object {
        //private val LOGGER: Logger = LoggerFactory.getLogger(ClassPathResourceProvider::class.java)
        private val fileSystemProviders: MutableMap<String, FileSystemProvider> = HashMap()
        private val lock = ReentrantLock()
    }
}
